/*
    Licensed to the Apache Software Foundation (ASF) under one
    or more contributor license agreements.  See the NOTICE file
    distributed with this work for additional information
    regarding copyright ownership.  The ASF licenses this file
    to you under the Apache License, Version 2.0 (the
    "License"); you may not use this file except in compliance
    with the License.  You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing,
    software distributed under the License is distributed on an
    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    KIND, either express or implied.  See the License for the
    specific language governing permissions and limitations
    under the License.
*/
package erwiki.api.parser;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.PushbackReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;

import org.apache.log4j.Logger;
import org.apache.oro.text.regex.Pattern;
import org.jdom2.Element;
import org.jdom2.Text;

import erwiki.LinkCollector;
import erwiki.StringTransmutator;
import erwiki.api.core.Engine;
import erwiki.api.rwc.RWC;
import erwiki.configuration.IWikiConfiguration;
import erwiki.util.TextUtil;

/**
 * Предоставляет абстрактный класс для экземпляров синтаксического анализатора.
 */
public abstract class MarkupParser {

	private static final Logger log = Logger.getLogger(MarkupParser.class);

	/**
	 * Определяет число символов, помещаемых обратно в поток.</br>
	 * По сути, это ограничивает размер одной строки.
	 */
	protected static final int PUSHBACK_BUFFER_SIZE = 10 * 1024;
	protected PushbackReader m_in;
	/** Текущее положение чтения во входном потоке. */
	private int m_pos = -1;

	protected final IWikiConfiguration wikiConfig;
	protected Engine m_engine;

	/** При необходимости сохраняет внутренние вики-ссылки. */
	protected List<LinkCollector> m_localLinkCollectors = new ArrayList<>();

	/** При необходимости сохраняет внешние вики-ссылки. */
	protected List<LinkCollector> m_externalLinkCollectors = new ArrayList<>();

	/** При необходимости сохраняет ссылки на вложения (attachment). */
	protected List<LinkCollector> m_attachmentLinkCollectors = new ArrayList<>();

	/** При необходимости сохраняет ссылки на неизвестные страницы. */
	protected List<LinkCollector> unknownPagesCollectors = new ArrayList<>();

	protected List<StringTransmutator> m_linkMutators = new ArrayList<>();

	protected List<HeadingListener> m_headingListenerChain = new ArrayList<>();

	protected boolean m_inlineImages = true;
	protected boolean m_parseAccessRules = true;

	/** Хранит шаблоны регулярных выражений названий файлов изображений. */
	private List<Pattern> m_inlineImagePatterns = null;
	protected LinkParsingOperations m_linkParsingOperations;

	public static final String HASHLINK = "hashlink";

	/** Name of the outlink image; relative path to the JSPWiki directory. */
	public static final String OUTLINK_IMAGE = "images/out.png";
	/** Outlink css class. */
	public static final String OUTLINK = "outlink";

	/**
	 * The value for anchor element <tt>class</tt> attributes when used for wiki page (normal) links.
	 * The value is "wikipage".
	 */
	public static final String CLASS_WIKIPAGE = "wikipage";

	/**
	 * The value for anchor element <tt>class</tt> attributes when used for edit page links. The value
	 * is "createpage".
	 */
	public static final String CLASS_CREATEPAGE = "createpage";

	/**
	 * The value for anchor element <tt>class</tt> attributes when used for interwiki page links. The
	 * value is "interwiki".
	 */
	public static final String CLASS_INTERWIKI = "interwiki";

	/**
	 * The value for anchor element <tt>class</tt> attributes when used for footnote links. The value is
	 * "footnote".
	 */
	public static final String CLASS_FOOTNOTE = "footnote";

	/**
	 * The value for anchor element <tt>class</tt> attributes when used for footnote links. The value is
	 * "footnote".
	 */
	public static final String CLASS_FOOTNOTE_REF = "footnoteref";

	/**
	 * The value for anchor element <tt>class</tt> attributes when used for external links. The value is
	 * "external".
	 */
	public static final String CLASS_EXTERNAL = "external";

	/**
	 * The value for anchor element <tt>class</tt> attributes when used for attachments. The value is
	 * "attachment".
	 */
	public static final String CLASS_ATTACHMENT = "attachment";

	@Deprecated
	public static final String[] CLASS_TYPES = { CLASS_WIKIPAGE, CLASS_CREATEPAGE, "", CLASS_FOOTNOTE,
			CLASS_FOOTNOTE_REF, "", CLASS_EXTERNAL, CLASS_INTERWIKI, CLASS_EXTERNAL, CLASS_WIKIPAGE, CLASS_ATTACHMENT };

	/**
	 * Constructs a MarkupParser. The subclass must call this constructor to set up the necessary bits
	 * and pieces.
	 *
	 * @param in The reader from which we are reading the bytes from.
	 */
	protected MarkupParser(Reader in) {
		m_engine = RWC.INSTANCE.getEngine();
		this.wikiConfig = m_engine.getWikiConfiguration();
		m_linkParsingOperations = new LinkParsingOperations();
		setInputReader(in);
	}

	/**
	 * Заменяет текущий входной поток символов новым.
	 *
	 * @param in Новый источник для ввода. Если значение равно <code>null</code>, то этот метод ничего
	 *           не делает.
	 * @return Старый поток ввода.
	 */
	public Reader setInputReader(Reader in) {
		Reader old = m_in;
		if (in != null) {
			m_in = new PushbackReader(new BufferedReader(in), PUSHBACK_BUFFER_SIZE);
		}

		return old;
	}

	/**
	 * Adds a hook for processing link texts. This hook is called when the link text is written into the
	 * output stream, and you may use it to modify the text. It does not affect the actual link, only
	 * the user-visible text.
	 *
	 * @param mutator The hook to call. Null is safe.
	 */
	public void addLinkTransmutator(StringTransmutator mutator) {
		if (mutator != null) {
			m_linkMutators.add(mutator);
		}
	}

	/**
	 * Adds a link collector for processing local links. The engine transforms both non-existing and
	 * existing page links.
	 *
	 * @param linkCollector The link collector to call. Null is safe.
	 */
	public void addLocalLinkCollector(LinkCollector linkCollector) {
		if (linkCollector != null) {
			m_localLinkCollectors.add(linkCollector);
		}
	}

	/**
	 * Adds a link collector for processing external links. This includes all http:// ftp://, etc.
	 * links, including inlined images.
	 *
	 * @param linkCollector The link collector to call. Null is safe.
	 */
	public void addExternalLinkCollector(LinkCollector linkCollector) {
		if (linkCollector != null) {
			m_externalLinkCollectors.add(linkCollector);
		}
	}

	/**
	 * Adds a link collector for processing attachment links.
	 *
	 * @param linkCollector The link collector to call. Null is safe.
	 */
	public void addAttachmentLinkCollector(LinkCollector linkCollector) {
		if (linkCollector != null) {
			m_attachmentLinkCollectors.add(linkCollector);
		}
	}

	/**
	 * Adds a link collector for processing unknown pages links.
	 *
	 * @param linkCollector The link collector to call. Null is safe.
	 */
	public void addUnknownPagesLinkCollector(LinkCollector linkCollector) {
		if (linkCollector != null) {
			unknownPagesCollectors.add(linkCollector);
		}
	}

	/**
	 * Adds a HeadingListener to the parser chain. It will be called whenever a parsed header is found.
	 *
	 * @param listener The listener to add.
	 */
	public void addHeadingListener(HeadingListener listener) {
		if (listener != null) {
			m_headingListenerChain.add(listener);
		}
	}

	/**
	 * Disables access rule parsing.
	 */
	public void disableAccessRules() {
		m_parseAccessRules = false;
	}

	public boolean isParseAccessRules() {
		return m_parseAccessRules;
	}

	/**
	 * Используйте этот метод, чтобы включить или отключить встраивание изображений.
	 *
	 * @param toggle Если <i>true</i>, то изображения встраиваются (согласно настройкам).</br>
	 *               Если <i>false</i>, то изображения не встраиваются; вместо этого они будут
	 *               рассматриваться как стандартные гиперссылки.
	 */
	public void enableImageInlining(boolean toggle) {
		m_inlineImages = toggle;
	}

	/**
	 * @return Статус - встраивать/нет изображения.
	 */
	public boolean isImageInlining() {
		return m_inlineImages;
	}

	abstract protected List<Pattern> initInlineImagePatterns();

	public List<Pattern> getInlineImagePatterns() {
		if (m_inlineImagePatterns == null) {
			m_inlineImagePatterns = initInlineImagePatterns();
		}
		return m_inlineImagePatterns;
	}

	/**
	 * Parses the document.
	 *
	 * @return the parsed document, as a WikiDocument
	 * @throws IOException If something goes wrong.
	 */
	public abstract WikiDocument parse() throws IOException;

	/**
	 * Возвращает текущее положение во входном потоке.</br>
	 * Перед началом чтения, значение будет равно -1.
	 *
	 * @return позиция чтения.
	 */
	protected int getPosition() {
		return m_pos;
	}

	/**
	 * Возвращает следующий токен потока.</br>
	 * Это самый вызываемый метод во всем парсере, поэтому он должен быть простым и скупым.
	 *
	 * @return Следующий токен потока; или, если поток завершился, -1.
	 * @throws IOException          Если случится что-то плохое.
	 * @throws NullPointerException Если еще не создан входной документ.
	 */
	protected final int nextToken() throws IOException, NullPointerException {
		m_pos++;
		return m_in.read();
	}

	/**
	 * Помещает обратно любой символ в текущий входной поток.</br>
	 * Однако, блокирует помещение флага завершения чтения (признак EOF, =-1).
	 *
	 * @param chr Код символа, который надо затолкнуть назад.
	 * @throws IOException Происходит в случае, если символ невозможно затолкнуть назад.
	 */
	protected void pushBack(int chr) throws IOException {
		if (chr != -1 && m_in != null) {
			m_pos--;
			m_in.unread(chr);
		}
	}

	/**
	 * Заталкивает обратно строку, в текущий входной поток.
	 *
	 * @param str
	 * @throws IOException
	 */
	protected void pushBack(String str) throws IOException {
		if (m_in != null) {
			char[] dst = new char[str.length()];
			str.getChars(0, str.length(), dst, 0);
			m_in.unread(dst);
			m_pos -= str.length();
		}
	}

	/**
	 * Создает HTML для сообщения об ошибке.</br>
	 * <b>NOTE:</b> Не добавляет его в документ, это следует сделать самостоятельно.
	 *
	 * @param error Строка сообщения об ошибке.
	 * @return Element, содержащий ошибку.
	 */
	public static Element makeError(String error) {
		return new Element("span").setAttribute("class", "error").addContent(error);
	}

	/**
	 * Добавьте <tt>&lt;span class="error"&gt;</tt> в скобки для сообщения об ошибке. Текст сообщения
	 * создается указанным элементом.
	 *
	 * @param hasError Элемент, который должен выдавать строку ошибки.
	 * @return Элемент, содержащий ошибку.
	 */
	public static Element makeError(Text hasError) {
		return new Element("span").setAttribute("class", "error").addContent(hasError);
	}

	/**
	 * Очищает вики-имя. Функциональность этого метода была изменена в версии 2.6, так что список
	 * разрешенных символов стал намного больше. Используйте {@link #wikifyLink(String)}, чтобы получить
	 * прежнее поведение.
	 * <p>
	 * [ This is a link ] -&gt; This is a link
	 *
	 * @param link Ссылка для очистки. Значение Null является безопасным и приводит к возвращению
	 *             значения <code>null</code>.
	 * @return Очищенная ссылка.
	 */
	@Deprecated // Возможно не требуется, например, для имени страницы.
	public static String cleanLink(String link) {
		return TextUtil.cleanString(link, TextUtil.PUNCTUATION_CHARS_ALLOWED);
	}

	/**
	 * Удаляет лишние устаревшие символы. Этот метод работает точно так же, как и метод cleanLink() до
	 * версии 2.6.
	 * <p>
	 * [ This is a link ] -&gt; ThisIsALink
	 *
	 * @param link Ссылка для очистки. Значение Null является безопасным и приводит к возвращению
	 *             значения <code>null</code>.
	 * @return Очищенная ссылка.
	 */
	public static String wikifyLink(String link) {
		return TextUtil.cleanString(link, TextUtil.LEGACY_CHARS_ALLOWED);
	}

}
